package Firepear::Carrot::Grammarian;

use warnings;
use strict;

use Firepear::Carrot::DOM;
use Firepear::Carrot::Lexer;
use Firepear::Carrot::Pragma::Document;
use Firepear::Carrot::Pragma::Include;
use Firepear::Carrot::Handler::Tag;
use Firepear::Carrot::Handler::Text;
use Firepear::Carrot::Handler::Trigger;
use Firepear::Carrot::Handler::Verbatim;
use Firepear::Carrot::Handler::VerbatimText;
use Firepear::Carrot::Util qw(commenter);

=head1 NAME

Firepear::Carrot::Grammarian - Check Carrot documents for well-formedness

=head1 VERSION

Version 1.000

=cut

our $VERSION = '1.000';


=head1 SYNOPSIS

    my $g = Firepear::Carrot::Grammarian->new( inputs => \@filelist,
                                               prefix => "foo" );
    $g->parse;

=head1 DESCRIPTION

The Grammarian is responsible for processing documents at a grammar
(that is: well-formedness) level. It does this with the assistance of
its attendant Handler modules, each of which manages one type of
Carrot markup.

=head1 METHODS

=head2 new

=cut

sub new {
  my ($class,%args) = @_;

  my $self = bless { context_stack => [ 'ROOT' ],
		     handlers => {},
		     mu       => $args{markup} || ['[',']',';',"'",'-'],
                     node     => undef,
                     prefix   => $args{prefix} || '.',
                     status   => 0,
		   }, $class;

  # create Lexer
  if (defined $args{filelist}[0]) {
    $self->{l} = Firepear::Carrot::Lexer->new( files => $args{filelist});
    $self->status(module => $self->{l}) if $self->{l}->status;
  } else {
    $self->status(status => 901);
  }

  # generate output filename if we don't already have one
  $self->{output} = (defined $args{output}) ?
    join('/', $self->{prefix}, $args{output}) :
    join ('/', $self->{prefix}, 'f_c_g');

  return $self;
}


=head2 parse

=cut

sub parse {
  my ($self) = @_;
  return undef if $self->status;
  my $l = $self->{l};

  until ($l->status == 99) {
    unless (defined $self->{doc}) {
      # if we don't have a document trigger, go looking for one
      $self->{doc} = $self->parse_document_trigger;
      return if ($self->status);
      # and if we succeeded in getting one, spawn the DOM
      $self->{dom} = Firepear::Carrot::DOM->new( root => $self->{doc}{attrs},
                                                 file => $self->{output} );
      if ($self->{dom}->status) # else, the usual
        { $self->status(module => $self->{dom}); return undef }

      # sucess; create handler stack and end parse loop
      $self->populate_handlers;
      return;
    }

    my $dom = $self->{dom};

    # get a token which isn't a blank line or full-line comment
    my $token = $self->metafetch($l);
    return undef unless defined $token;

    # run handles() check/dispatch; update context
    my @handler_list = qw(verbatim include trigger tag text);
    my $handlers = $self->{handlers};
    my $handler;
    for (@handler_list) {
      if ($handlers->{$_}->handles($token))
        { $handler = $handlers->{$_}; last }
    }
    $handler = $handlers->{verbatimtext} if ($self->context eq "VERBATIM");
    push @{$self->{context_stack}}, $handler->{type};

    # let selected handler assemble us a node
    my $node = $handler->lex($l);
    unless (defined $node)
      { $self->status(module => $handler); return undef }

    # insert node into DOM; push context and move DOM ptr if handler status is 2
    my $nodeid = $dom->add($node);
    unless (defined $nodeid)
      { $self->status(module => $dom); return undef }
    if ($handler->status == 2) {
        push @{$self->{context_stack}}, $node->{type};
        $dom->jmp($nodeid);
    }

    # pop context and move DOM ptr to parent if handler status is 3
    if ($handler->status == 3) {
        pop @{$self->{context_stack}};
        $dom->jmp('parent');
    }
  }
  $self->status( status=> 1) if ($l->status == 99);
}


=head2 parse_document_trigger

=cut

sub parse_document_trigger {
  my ($self) = @_;
  my $l = $self->{l};

  # spawn document pragma trigger
  my $d = Firepear::Carrot::Pragma::Document->new(mu => $self->{mu});
  if ($d->status)
    { $self->status(module => $l); return undef }

    # get a token which isn't a blank line or full-line comment
  my $token = $self->metafetch($l);
  return undef unless defined $token;

  # the first real token must be the document trigger
  if ($d->handles($token)) {
    my $node = $d->lex($l);
    $self->status(module => $d) unless defined $node;
    return $node;
  } else {
    $self->status(status => 101);
    return undef;
  }
}


=head2 metafetch

=cut

sub metafetch {
  my ($self, $l) = @_;
  my $token = '';
  until ($token =~ /\S/ or $l->status > 1) {
    $token = $l->fetch;
    unless (defined $token)
      { $self->status(module => $l); return undef }
    $token = $self->commenter($l, ($self->{mu}[2] x 3));
  }
  return $token;
}


=head2 populate_handlers

=cut

sub populate_handlers {
  my ($self) = @_;

  $self->{handlers}{verbatim} =
    Firepear::Carrot::Handler::Verbatim->new(mu => $self->{mu});
  $self->{handlers}{include} =
    Firepear::Carrot::Pragma::Include->new(mu => $self->{mu});
  $self->{handlers}{trigger} =
    Firepear::Carrot::Handler::Trigger->new(mu => $self->{mu});
  $self->{handlers}{tag} =
    Firepear::Carrot::Handler::Tag->new(mu => $self->{mu});
  $self->{handlers}{text} =
    Firepear::Carrot::Handler::Text->new(mu => $self->{mu});
  $self->{handlers}{verbatimtext} =
    Firepear::Carrot::Handler::VerbatimText->new(mu => $self->{mu});
}


=head2 context

=cut

sub context {
my ($self)= @_;
  return $self->{context_stack}[-1];
}

=head2 status

With no arguments, returns the Grammarian's current status.

With the C<status> argument and a value passed, sets the status to
that value.

With the C<module> argument and a subordinate object passed, sets the
Grammarian's status to the appropriate value based on object type and
status.

=cut

sub status {
  my ($self,%args) = @_;
  if (defined $args{status})
    { $self->{status} = $args{status}; return }
  if (defined $args{module}) {
    my $module = ref $args{module};
    $self->{status} = 1000 + $args{module}->status
      if ($module =~ /Lexer/);
    $self->{status} = 2000 + $args{module}->status
      if ($module =~ /DOM/);
    $self->{status} = 3000 + $args{module}->status
      if ($module =~ /(Handler|Pragma)/);
  }
  return $self->{status};;
}



=head1 AUTHOR

Shawn Boyette, C<< <mdxi@cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests at
L<http://carrot.googlecode.com/>.  I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.

=head1 COPYRIGHT & LICENSE

Copyright 2008 Shawn Boyette

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.

    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.

    * Neither the name of the <ORGANIZATION> nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=cut

1; # End of Firepear::Carrot::Grammarian
